TypeScript keyof 和 泛型的使用
写 TypeScript 什么都好,就是它的类型系统有时候太麻烦了,所以这篇笔记记录一下遇到的各种类型问题如何处理
处理类型的常用操作符一览表
关键字 | 作用 |
---|---|
instanceof | 实例判断 |
typeof | 类型判断 |
as | 类型强制转换 |
is | 断言返回布尔类型 |
? | 条件类型 |
keyof | 键名索引 |
in | 映射 |
infer | 声明待推断的类型 |
<> | 泛型 |
type | 别名 |
| | 联合类型 |
& | 交叉类型 |
keyof 关键字的使用
该操作符可以用于获取某种类型的所有键,其返回类型是联合类型
interface Person {
name: string;
age: number;
}
type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join"
type K3 = keyof { [x: string]: Person }; // string | number
遍历对象的属性⭐
在 TypeScript 里面,当需要遍历对象的时候,经常就会遇到下图所示的错误提示。
function test (foo: object) {
for (let key in foo) {
console.log(foo[key]); // typescript错误提示
// do something
}
}
解决方法:
把对象声明 as any
function test (foo: object) {
for (let key in foo) {
console.log((foo as any)[key]); // 报错消失
// do something
}
}
但是这个方法有点像是直接绕过了 typescript 的校验机制,失去了使用 typescript 的初心,这不是我们想要的。
给对象声明一个接口⭐
interface SimpleKeyValueObject {
[key: string]: any
}
function test (foo: SimpleKeyValueObject) {
for (let key in foo) {
console.log(foo[key]); // 报错消失
// do something
}
}
这个可以针对比较常见的对象类型,特别是一些工具方法。
但是这个不够骚,能不能再厉害点呢~
使用泛型
function test<T extends object> (foo: T) {
for (let key in foo) {
console.log(foo[key]); // 报错消失
// do something
}
}
当我们需要对 foo 的 key 进行约束时,我们可以使用下面的第 4 种方法。
使用 keyof
interface Ifoo{
name: string;
age: number;
weight: number;
}
function test (opt: Ifoo) {
let key: (keyof Ifoo);
for (key in opt) {
console.log(opt[key]); // 报错消失
// do something
}
}
Intersection Types(交叉类型)
交叉类型是一种将多种类型组合为一种类型的方法。 这意味着你可以将给定的类型 A 与类型 B 或更多类型合并,并获得具有所有属性的单个类型。
type LeftType = {
id: number;
left: string;
};
type RightType = {
id: number;
right: string;
};
type IntersectionType = LeftType & RightType;
function showType(args: IntersectionType) {
console.log(args);
}
showType({ id: 1, left: 'test', right: 'test' });
// Output: {id: 1, left: "test", right: "test"}
IntersectionType 组合了两种类型-LeftType和RightType,并使用 & 符号形成了交叉类型。
Union Types(联合类型)
联合类型使你可以赋予同一个变量不同的类型
type UnionType = string | number;
function showType(arg: UnionType) {
console.log(arg);
}
showType('test');
// Output: test
showType(7);
// Output: 7
Pick 从类型抽取属性为新类型
Pick 方法允许你从一个已存在的类型 T 中选择一些属性作为 K,从而创建一个新类型
即 抽取一个类型/接口中的一些子集作为一个新的类型
T
代表要抽取的对象
K
有一个约束:一定是来自 T 所有属性字面量的联合类型
新的 类型/属性 一定要从 K 中选取
源码实现
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
- T 是要从中选择元素的类型
- K 是要选择的属性(可以使使用联合类型来选择多个字段)
使用例:
interface PickType {
id: number;
firstName: string;
lastName: string;
}
function showType(args: Pick<PickType, 'firstName' | 'lastName'>) {
console.log(args);
}
showType({ firstName: 'John', lastName: 'Doe' });
// Output: {firstName: "John"}
showType({ id: 3 });
// Error: Object literal may only specify known properties, and 'id' does not exist in type 'Pick<PickType, "firstName" | "lastName">'
Omit 删除部分属性
Omit 的作用与 Pick 类型正好相反。 不是选择元素,而是从类型 T 中删除 K 个属性。
interface PickType {
id: number;
firstName: string;
lastName: string;
}
function showType(args: Omit<PickType, 'firstName' | 'lastName'>) {
console.log(args);
}
showType({ id: 7 });
// Output: {id: 7}
showType({ firstName: 'John' });
// Error: Object literal may only specify known properties, and 'firstName' does not exist in type 'Pick<PickType, "id">'
Extract 取类型的交集
Extract 提取 T 中可以赋值给 U 的类型(取交集)
Extract 允许你通过选择两种不同类型中的共有属性来构造新的类型。 也就是从 T 中提取所有可分配给 U 的属性。
interface FirstType {
id: number;
firstName: string;
lastName: string;
}
interface SecondType {
id: number;
address: string;
city: string;
}
type ExtractType = Extract<keyof FirstType, keyof SecondType>;
// Output: "id"
在上面的代码中,FirstType 接口和 SecondType 接口,都存在 id:number
属性。 因此,通过使用 Extract,即提取出了新的类型 {id:number}
。
Exclude 排除交集
Exclude 与上相反,从 T 中剔除可以赋值给 U 的类型。
与 Extract 不同,Exclude 通过排除两个不同类型中已经存在的共有属性来构造新的类型。 它会从 T 中排除所有可分配给 U 的字段。
interface FirstType {
id: number;
firstName: string;
lastName: string;
}
interface SecondType {
id: number;
address: string;
city: string;
}
type ExcludeType = Exclude<keyof FirstType, keyof SecondType>;
// Output; "firstName" | "lastName"
Mapped Types(旧映射为新类型)⭐
映射类型允许你从一个旧的类型,生成一个新的类型。
这里先来看下上面的
type StringMap<T> = {
[P in keyof T]: string;
};
function showType(arg: StringMap<{ id: number; name: string }>) {
console.log(arg);
}
showType({ id: 1, name: 'Test' });
// Error: Type 'number' is not assignable to type 'string'.
showType({ id: 'testId', name: 'This is a Test' });
// Output: {id: "testId", name: "This is a Test"}
StringMap
会将传入的任何类型转换为字符串。 就是说,如果我们在函数 showType()
中使用它,则接收到的参数必须是字符串-否则,TypeScript 将引发错误。
错误场景1
开发中使用 typescript 的时候,经常会遇到使用 Object.keys
这个方法报错的情况,报错如下:
补充:Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。
// 数组
var obj = {'a':'123','b':'345'};
console.log(Object.keys(obj)); //['a','b']
而到了 TS 会发生如下报错
var foo = {
a: '1',
b: '2'
}
var getPropertyValue = Object.keys(foo).map(item => foo[item]) // 这里会有typescript的错误提示
解决方案:通过 keyof
的方式可以获取 ts 类型的属性 key 的值
var foo = {
a: '1',
b: '2'
}
// 这里 typeof foo => foo的类型 等同于 interface Foo { a: string; b: string; }
// typeof foo === Foo,这里只所以用 typeof foo,因为这样方便,对于不想写interface的直接量对象很容易获取它的类型
// keyof typeof foo 这里只获取 Foo 的类型的 key 值,注意这个 keyof 后面一定是 typescript 的类型
type FooType = keyof typeof foo;
var getPropertyValue = Object.keys(foo).map(item => foo[item as FooType])
错误场景2
var foo = {
a: '1',
b: '2'
}
function getPropertyValue(obj, key) { // 这里也会提示obj会有any类型
return obj[key]
}
可以通过以下的方法解决:
var foo = {
a: '1',
b: '2'
}
// 这里声明了两个泛型 T 和 K
// T 代表函数第一个参数的类型,K 代表函数第二个参数的类型这个类型指向第一个参数类型中包含的key的值
function getPropertyValue<T, K extends keyof T>(obj:T, key:K):T[K] {
return obj[key]
}
getPropertyValue(foo, 'a')